/**
 *  Copyright (c) Filipe Manana <fdmanana@gmail.com>
 *  All rights reserved.
 * 
 *  This file is part of jsdet.
 *
 *  jsdet is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published
 *  by the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  jsdet is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with jsdet.  If not, see <http://www.gnu.org/licenses/>.
 *
 */


package jsdet.parsing.reporting;

import java.text.*;
import java.util.*;

import jsdet.parsing.GlobalParsingStats;
import jsdet.parsing.enums.ClassMemberType;


/**
 * 
 * @author Filipe Manana
 *
 */
public class XmlReporter extends AbstractReporter {

    private static final String NEWLINE = "\r\n";
    private static final SimpleDateFormat dateFormatter = new SimpleDateFormat("E dd MMM yyyy HH:mm:ss");

    // XML elements
    private static final String RESULTS_ELEMENT = "results";
    private static final String FUNCTIONS_ELEMENT = "functions";
    private static final String FUNCTION_ELEMENT = "function";
    private static final String CLOSURE_OBJECTS_ELEMENT = "closureObjects";
    private static final String CLOSURE_OBJECT_ELEMENT = "closureObject";
    private static final String CLASSES_ELEMENT = "classes";
    private static final String CLASS_ELEMENT = "class";
    private static final String MEMBER_ELEMENT = "member";
    private static final String FILES_ELEMENT = "files";
    private static final String FILE_ELEMENT = "file";

    // XML attributes
    private static final String RUN_ON_ATTR = "from";
    private static final String CLASS_COUNT_ATTR = "classes";
    private static final String FUNCTION_COUNT_ATTR = "functions";
    private static final String CLOSURE_OBJECT_COUNT_ATTR = "closureObjects";
    private static final String NAME_ATTR = "name";
    private static final String FILE_ATTR = "file";
    private static final String TYPE_ATTR = "type";
    private static final String NONFUNCTION_MEMBERS_COUNT_ATTR ;
    private static final String FUNCTION_MEMBERS_COUNT_ATTR;
    private static final String EXTENDS_ATTR = "extends";
    private static final String INHERITED_FROM_ATTR = "inheritedFrom";
    private static final String OVERRIDES_ATTR = "overrides";
    private static final String INHERITED_COUNT_ATTR = "inheritedMembers";
    private static final String OVERRIDING_COUNT_ATTR = "overridingMembers";
    private static final String OWN_COUNT_ATTR = "ownMembers";
    private static final String SORTING_ATTR = "sorting";

    static {
        String memberTypeLabel = ClassMemberType.NON_FUNCTION.getLabel();

        NONFUNCTION_MEMBERS_COUNT_ATTR = memberTypeLabel + "Members";

        memberTypeLabel = ClassMemberType.FUNCTION.getLabel();

        FUNCTION_MEMBERS_COUNT_ATTR = memberTypeLabel + "Members";
    }


    @Override
    public void reportGlobalParsingStats(GlobalParsingStats stats) {
        Map<String, String> attributes = new LinkedHashMap<String, String>();

        classesAlreadyReported.clear();

        System.out.print("<?xml version=\"1.0\"?>" + NL(1));

        attributes.put(RUN_ON_ATTR, dateFormatter.format(new Date()));

        if ( areFunctionsReported() ) {
            attributes.put(FUNCTION_COUNT_ATTR, "" + stats.getFunctions().size());
        }

        if ( areClosureObjectsReported() ) {
            attributes.put(CLOSURE_OBJECT_COUNT_ATTR, "" + stats.getClosureObjects().size());
        }

        attributes.put(CLASS_COUNT_ATTR, "" + stats.getClasses().size());

        System.out.print(NL(1));
        System.out.print(startElement(0, RESULTS_ELEMENT, attributes));

        if ( areFunctionsReported() ) {
            System.out.print(NL(1));
            reportFunctions(1, stats);
        }

        if ( areClosureObjectsReported() ) {
            System.out.print(NL(1));
            reportClosureObjects(1, stats);
        }

        System.out.print(NL(1));

        reportClasses(1, stats);

        System.out.print(NL(1));

        System.out.print(endElement(0, RESULTS_ELEMENT));
    }


    private void reportFunctions(int indentLevel, GlobalParsingStats stats) {
        Set<String> functions = stats.getFunctions();

        if ( functions.isEmpty() ) {
            System.out.print(startElement(indentLevel, FUNCTIONS_ELEMENT, true));
            return;
        }

        System.out.print(startElement(indentLevel, FUNCTIONS_ELEMENT));

        for ( String function : functions ) {
            reportFunction(indentLevel + 1, function, stats);
        }

        System.out.print(endElement(indentLevel, FUNCTIONS_ELEMENT));
    }


    private void reportFunction(int indentLevel, String name, GlobalParsingStats stats) {
        Map<String, String> attributes = new LinkedHashMap<String, String>();

        attributes.put(NAME_ATTR, name);

        if ( areFileNamesReported() ) {
            attributes.put(FILE_ATTR, stats.getFunctionFile(name));
        }

        System.out.print(startElement(indentLevel, FUNCTION_ELEMENT, attributes, true));
    }


    private void reportClosureObjects(int indentLevel, GlobalParsingStats stats) {
        Set<String> closures = stats.getClosureObjects();

        if ( closures.isEmpty() ) {
            System.out.print(startElement(indentLevel, CLOSURE_OBJECTS_ELEMENT, true));
            return;
        }

        System.out.print(startElement(indentLevel, CLOSURE_OBJECTS_ELEMENT));

        for ( String closure : closures ) {
            reportClosureObject(indentLevel + 1, closure, stats);
        }

        System.out.print(endElement(indentLevel, CLOSURE_OBJECTS_ELEMENT));
    }


    private void reportClosureObject(int indentLevel, String closureName, GlobalParsingStats stats) {
        int nonfunctionMembersCount = stats.getClosureObjectMembers(closureName, ClassMemberType.NON_FUNCTION).size();
        int functionMembersCount = stats.getClosureObjectMembers(closureName, ClassMemberType.FUNCTION).size();
        Map<String, String> attributes = new LinkedHashMap<String, String>();

        attributes.put(NAME_ATTR, closureName);
        attributes.put(NONFUNCTION_MEMBERS_COUNT_ATTR, "" + nonfunctionMembersCount);
        attributes.put(FUNCTION_MEMBERS_COUNT_ATTR, "" + functionMembersCount);

        if ( areFileNamesReported() ) {
            attributes.put(FILE_ATTR, stats.getClosureObjectFile(closureName));
        }

        if ( reportNamesOnly ) {
            System.out.print(startElement(indentLevel, CLOSURE_OBJECT_ELEMENT, attributes, true));
            return;
        }

        System.out.print(startElement(indentLevel, CLOSURE_OBJECT_ELEMENT, attributes));

        for ( String member : stats.getClosureObjectMembers(closureName, ClassMemberType.NON_FUNCTION) ) {
            reportClosureObjectMember(indentLevel + 1, closureName, member, stats);
        }

        for ( String member : stats.getClosureObjectMembers(closureName, ClassMemberType.FUNCTION) ) {
            reportClosureObjectMember(indentLevel + 1, closureName, member, stats);
        }

        System.out.print(endElement(indentLevel, CLOSURE_OBJECT_ELEMENT));
    }


    private void reportClosureObjectMember(int indentLevel, String closureName, String memberName,
            GlobalParsingStats stats) {

        ClassMemberType type = stats.getClosureObjectMemberType(closureName, memberName);
        Map<String, String> attributes = new LinkedHashMap<String, String>();

        attributes.put(NAME_ATTR, memberName);
        attributes.put(TYPE_ATTR, type.getLabel());

        System.out.print(startElement(indentLevel, MEMBER_ELEMENT, attributes, true));
    }


    private void reportClasses(int indentLevel, GlobalParsingStats stats) {
        Set<String> classes = null;
        Map<String, String> attributes = new LinkedHashMap<String, String>();

        attributes.put(SORTING_ATTR, classesSortOrder.getLabel());

        switch ( classesSortOrder ) {
            case ASC:
                classes = stats.getClassesOrderedByHierarchyLevelAsc();
                break;

            case DESC:
                classes = stats.getClassesOrderedByHierarchyLevelDesc();
                break;

            case DEFAULT:
            default:
                classes = stats.getClasses();
        }

        if ( classes.isEmpty() ) {
            System.out.print(startElement(indentLevel, CLASSES_ELEMENT, attributes, true));
            return;
        }

        System.out.print(startElement(indentLevel, CLASSES_ELEMENT, attributes));

        for ( String className : classes ) {

            if ( classesAlreadyReported.contains(className) ) {
                continue;
            }

            if ( !isClassInFilter(className) ) {
                continue;
            }

            if ( classesSortOrder == ClassesSortOrder.DEFAULT ) {
                List<String> upperClasses = stats.getClassBaseHierarchy(className);

                for ( int i = upperClasses.size() - 1; i >= 0; i-- ) {
                    String upperClass = upperClasses.get(i);

                    if ( !stats.hasClass(upperClass) ) {
                        continue;
                    }

                    if ( classesAlreadyReported.contains(upperClass) ) {
                        continue;
                    }

                    if ( !isClassInFilter(upperClass) ) {
                        continue;
                    }

                    reportClass(indentLevel + 1, upperClass, stats);
                    classesAlreadyReported.add(upperClass);
                }
            }

            reportClass(indentLevel + 1, className, stats);
            classesAlreadyReported.add(className);
        }

        System.out.print(endElement(indentLevel, CLASSES_ELEMENT));
    }


    private void reportClass(int indentLevel, String className, GlobalParsingStats stats) {
        int nonfunctionMembersCount = stats.getClassMembers(className, ClassMemberType.NON_FUNCTION).size();
        int functionMembersCount = stats.getClassMembers(className, ClassMemberType.FUNCTION).size();
        String baseClass = stats.getClassBase(className);
        int inheritedMembersCount = stats.getClassInheritedMembers(className).size();
        int memberCount = stats.getClassMembers(className).size() + inheritedMembersCount;
        Map<String, String> attributes = new LinkedHashMap<String, String>();

        attributes.put(NAME_ATTR, className);

        if ( baseClass != null ) {
            attributes.put(EXTENDS_ATTR, baseClass);
        }

        attributes.put(OWN_COUNT_ATTR, "" + stats.getClassUniqueMembers(className).size());
        attributes.put(OVERRIDING_COUNT_ATTR, "" + stats.getClassOverridingMembers(className).size());

        if ( areInheritedMembersReported() ) {
            nonfunctionMembersCount += stats.getClassInheritedMembers(className, ClassMemberType.NON_FUNCTION).size();
            functionMembersCount += stats.getClassInheritedMembers(className, ClassMemberType.FUNCTION).size();

            attributes.put(INHERITED_COUNT_ATTR, "" + inheritedMembersCount);
        }

        attributes.put(NONFUNCTION_MEMBERS_COUNT_ATTR, "" + nonfunctionMembersCount);
        attributes.put(FUNCTION_MEMBERS_COUNT_ATTR, "" + functionMembersCount);

        if ( reportNamesOnly || (memberCount == 0) ) {
            System.out.print(startElement(indentLevel, CLASS_ELEMENT, attributes, true));
            return;
        }

        System.out.print(startElement(indentLevel, CLASS_ELEMENT, attributes));

        // non-function members first
        for ( String member : stats.getClassUniqueMembers(className, ClassMemberType.NON_FUNCTION) ) {
            reportClassMember(indentLevel + 1, className, member, stats);
        }

        for ( String member : stats.getClassOverridingMembers(className, ClassMemberType.NON_FUNCTION) ) {
            reportClassMember(indentLevel + 1, className, member, stats);
        }

        if ( areInheritedMembersReported() ) {
            for ( String member : stats.getClassInheritedMembers(className, ClassMemberType.NON_FUNCTION) ) {
                reportClassMember(indentLevel + 1, className, member, stats);
            }
        }

        // and then function members
        for ( String member : stats.getClassUniqueMembers(className, ClassMemberType.FUNCTION) ) {
            reportClassMember(indentLevel + 1, className, member, stats);
        }

        for ( String member : stats.getClassOverridingMembers(className, ClassMemberType.FUNCTION) ) {
            reportClassMember(indentLevel + 1, className, member, stats);
        }

        if ( areInheritedMembersReported() ) {
            for ( String member : stats.getClassInheritedMembers(className, ClassMemberType.FUNCTION) ) {
                reportClassMember(indentLevel + 1, className, member, stats);
            }
        }

        System.out.print(endElement(indentLevel, CLASS_ELEMENT));
    }


    private void reportClassMember(int indentLevel, String className, String memberName, GlobalParsingStats stats) {
        String upperClass = stats.findFirstUpperClassWithMember(className, memberName);
        boolean overriding = stats.isClassMemberOverriding(className, memberName);
        boolean inherited = stats.classHasInheritedMember(className, memberName);
        ClassMemberType type = stats.getClassMemberType(inherited ? upperClass : className, memberName);
        Map<String, String> attributes = new LinkedHashMap<String, String>();

        attributes.put(NAME_ATTR, memberName);
        attributes.put(TYPE_ATTR, type.getLabel());

        if ( inherited ) {
            attributes.put(INHERITED_FROM_ATTR, upperClass);
        }
        else if ( overriding ) {
            attributes.put(OVERRIDES_ATTR, upperClass + "." + memberName);
        }

        if ( areFileNamesReported() ) {
            System.out.print(startElement(indentLevel, MEMBER_ELEMENT, attributes));

            Set<String> files = null;

            if ( inherited ) {
                files = stats.getFilesWhereClassMemberIsDefined(upperClass, memberName);
            }
            else {
                files = stats.getFilesWhereClassMemberIsDefined(className, memberName);
            }

            if ( files.isEmpty() ) {
                System.out.print(startElement(indentLevel + 1, FILES_ELEMENT, true));
            }
            else {
                System.out.print(startElement(indentLevel + 1, FILES_ELEMENT));

                for ( String file : files ) {
                    Map<String, String> fileAttributes = new LinkedHashMap<String, String>();

                    fileAttributes.put(NAME_ATTR, file);

                    System.out.print(startElement(indentLevel + 2, FILE_ELEMENT, fileAttributes, true));
                }

                System.out.print(endElement(indentLevel + 1, FILES_ELEMENT));
            }

            System.out.print(endElement(indentLevel, MEMBER_ELEMENT));
        }
        else {
            System.out.print(startElement(indentLevel, MEMBER_ELEMENT, attributes, true));
        }
    }


    private String startElement(int indentLevel, String name) {
        return startElement(indentLevel, name, null, false);
    }


    private String startElement(int indentLevel, String name, boolean close) {
        return startElement(indentLevel, name, null, close);
    }


    private String startElement(int indentLevel, String name, Map<String, String> attributes) {
        return startElement(indentLevel, name, attributes, false);
    }


    private String startElement(int indentLevel, String name, Map<String, String> attributes, boolean close) {
        StringBuffer sb = new StringBuffer("");

        sb.append(indent(indentLevel));
        sb.append("<");
        sb.append(name);

        if ( attributes != null ) {
            for ( String attr : attributes.keySet() ) {
                String attrValue = attributes.get(attr);

                sb.append(" ");
                sb.append(attr);
                sb.append("=\"");
                sb.append(attrValueEscape(attrValue));
                sb.append("\"");
            }
        }

        if ( close ) {
            sb.append(" /");
        }

        sb.append(">");
        sb.append(NL(1));

        return sb.toString();
    }


    private String endElement(int indentLevel, String name) {
        StringBuffer sb = new StringBuffer("");

        sb.append(indent(indentLevel));
        sb.append("</");
        sb.append(name);
        sb.append(">");
        sb.append(NL(1));

        return sb.toString();
    }


    private String indent(int level) {
        StringBuffer sb = new StringBuffer("");

        for ( int i = 0; i < level; i++ ) {
            sb.append("    "); // 4 spaces
        }

        return sb.toString();
    }


    private String NL(int count) {
        StringBuffer sb = new StringBuffer("");

        for ( int i = 0; i < count; i++ ) {
            sb.append(NEWLINE);
        }

        return sb.toString();
    }


    private String attrValueEscape(String str) {
        str = str.replaceAll("<", "&lt;");
        str = str.replaceAll(">", "&gt;");
        str = str.replaceAll("\"", "&quot;");
        str = str.replaceAll("'", "&#39;");
        str = str.replaceAll("&(?!(?:(?:#\\d+)|\\w+);)", "&amp;");

        return str;
    }
}
